import { error } from '../../../output.js'
import { toToken } from '../../stream/token.js'
import parser from '../../parser/index.js'
import interpreter, { init } from '../../interpreter/index.js'
import Runtime from '../../../runtime.js'

const handleIdentifierForNativeFunction = (runtime, ast, arg, args) => {
  if (arg.type === 'Identifier') {
    if (runtime.scopes.has(ast.filepath)) {
      const scope = runtime.scopes.get(ast.filepath)
      const constant = scope.get('constant')
      const variable = scope.get('variable')

      if (constant.has(arg.value)) {
        args.push(constant.get(arg.value))
        return true
      }

      if (variable.has(arg.value)) {
        args.push(variable.get(arg.value))
        return true
      }

      if (runtime.parent !== null) {
        return handleIdentifierForNativeFunction(runtime.parent, ast, arg, args)
      }

      error(`Can't find variable: ${ arg.value }`)
      return false
    }

    if (runtime.variables.has(arg.value)) {
      args.push(runtime.variables.get(arg.value))
      return true
    }

    if (runtime.parent !== null) {
      return handleIdentifierForNativeFunction(runtime.parent, ast, arg, args)
    }

    error(`Can't find variable: ${ arg.value }`)
    return false
  }
  return false
}

const handleIdentifierForFunction = (node, ast, runtime, param, index, _constant) => {
  if (node.arguments[index].type === 'Identifier') {
    if (runtime.scopes.has(ast.filepath)) {
      const scope = runtime.scopes.get(ast.filepath)
      const constant = scope.get('constant')
      const variable = scope.get('variable')

      if (constant.has(node.arguments[index].value)) {
        _constant.set(param.name, constant.get(node.arguments[index].value))
        return
      }

      if (variable.has(node.arguments[index].value)) {
        _constant.set(param.name, variable.get(node.arguments[index].value))
        return
      }

      if (runtime.parent !== null) {
        handleIdentifierForFunction(node, ast, runtime.parent, param, index, _constant)
        return
      }

      error(`Can't find variable: ${ node.arguments[index].value }`)
      return
    }

    if (runtime.variables.has(node.arguments[index].value)) {
      _constant.set(param.name, runtime.variables.get(node.arguments[index].value))
      return
    }

    if (runtime.parent !== null) {
      handleIdentifierForFunction(node, ast, runtime.parent, param, index, _constant)
      return
    }

    error(`Can't find variable: ${ node.arguments[index].value }`)
  }
}

const run = async (runtime, ast, node) => {
  if (node.type !== 'CallExpression') return

  const _run = async method => {
    const params = method.params
    const type = typeof method.body === 'function' ? 'NativeFunctionBody' : 'FunctionBody'

    if (type === 'NativeFunctionBody') {
      const args = []

      for (let i = 0; i < node.arguments.length; i++) {
        const arg = node.arguments[i]

        if (arg.type === 'Literal') {
          args.push(arg.value)
          continue
        } else if (arg.type === 'BlockStatement') {
          const _run = async (...args) => {
            const _tokens = toToken(arg.body)
            const _ast = parser(_tokens, ast.filepath, 'function')
            const _runtime = new Runtime()

            init(_runtime, _ast)

            const constant = _runtime.scopes.get(ast.filepath).get('constant')

            _runtime.parent = runtime
            _runtime.config = runtime.config

            return await interpreter(_runtime, _ast)
          }
          args.push(_run)
          continue
        }

        if (handleIdentifierForNativeFunction(runtime, ast, arg, args)) continue

        if (arg.type === 'CallExpression') {
          const result = await run(runtime, ast, arg)
          args.push(result)
          continue
        }

        error(`Can't handle type: ${ arg.type }`)
      }

      return await method.body(...args)
    }

    if (type === 'FunctionBody') {
      const _tokens = toToken(method.body)

      const _ast = parser(_tokens, ast.filepath, 'function')

      const _runtime = new Runtime()

      init(_runtime, _ast)

      const constant = _runtime.scopes.get(ast.filepath).get('constant')

      _runtime.parent = runtime
      _runtime.config = runtime.config

      params.forEach((param, index) => {
        if (node.arguments.length <= index) {
          error(`Missing argument: ${ param.name }`)
        }
        if (node.arguments[index].type === 'Literal') {
          constant.set(param.name, node.arguments[index].value)
          return
        }
        if (node.arguments[index].type === 'Identifier') {
          handleIdentifierForFunction(node, ast, _runtime, param, index, constant)
          return
        }
        error(`Can't handle type: ${ param.type }`)
      })

      return await interpreter(_runtime, _ast)
    }

    error(`Can't handle type: ${ type }`)
  }

  const getMethod = (runtime, name, alias) => {
    if (alias === null || alias === '') {
      if (runtime.methods.has(name)) return runtime.methods.get(name)
      if (runtime.parent !== null) return getMethod(runtime.parent, name, alias)
    } else {
      if (runtime.methods.has(`${ alias }.${ name }`)) return runtime.methods.get(`${ alias }.${ name }`)
      if (runtime.parent !== null) return getMethod(runtime.parent, name, alias)
    }
    return null
  }

  const method = getMethod(runtime, node.callee.name, node.alias)

  if (method !== null) return await _run(method)

  error(`Can't find method: ${ (node.alias === null || node.alias === '') ? '' : `${ node.alias }.` }${ node.callee.name }`)
}

export default run
